﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace SSharp.Core.Reader {
	/// <summary>
	/// Provides access to a stream char-by-char.
	/// The stream is returned as a blocking enumeration of characters, read by line.
	/// 
	/// See CharReaderMode for important comments.
	/// </summary>
	public class CharReader : IDisposable {
		private const int DefaultBlockSize = 1024;

		private StreamReader reader;
		private CharReaderMode mode = CharReaderMode.Line;
		private int blockSize = DefaultBlockSize;
		private Boolean started = false;

		public CharReader(Stream stream) {
			reader = new StreamReader(stream);
		}

		/// <summary>
		/// When called, starts reading the stream. Cannot be called again.
		/// </summary>
		public IEnumerable<char> GetChars() {
			if (started)
				throw new InvalidOperationException("CharReader.GetChars() can only be called once");

			started = true;
			if (mode == CharReaderMode.Line) {
				return GetCharsByLine();
			} else {
				return GetCharsByBlock();
			}
		}

		/// <summary>
		/// GetChars, when mode == CharReaderMode.Line.
		/// Reads line by line, returns each char and newlines between them.
		/// </summary>
		private IEnumerable<char> GetCharsByLine() {
			while (true) {
				string line = reader.ReadLine();
				if (line == null)
					yield break;

				foreach (char c in line)
					yield return c;
				yield return '\n';
			}
		}

		/// <summary>
		/// GetChars, when mode == CharReaderMode.Block
		/// Reads blocks of (blockSize) character, and returns each char. Does not alter the stream at all.
		/// </summary>
		/// <returns></returns>
		private IEnumerable<char> GetCharsByBlock() {
			int length = BlockSize;
			char[] buffer = new char[length];
			while (true) {
				int count = reader.Read(buffer, 0, length);
				if (count == 0) {
					if (reader.EndOfStream)
						yield break;
				}

				for (int i = 0; i < count; i++)
					yield return buffer[i];
			}
		}

		/// <summary>
		/// Sets the reading mode to Line or Block.
		/// 
		/// Cannot be called after reading has started.
		/// </summary>
		public CharReaderMode Mode {
			get { return mode; }
			set {
				if (value != CharReaderMode.Line && value != CharReaderMode.Block)
					throw new ArgumentException("Invalid CharReaderMode value: " + value);
				
				if (started)
					throw new InvalidOperationException("Cannot set CharReaderMode after reading has started.");

				mode = value;
			}
		}

		/// <summary>
		/// Sets the block size. Only affects reading if Mode == CharReaderMode.Block.
		/// 
		/// Cannot be called after reading has started.
		/// </summary>
		public int BlockSize {
			get { return blockSize; }
			set {
				if (value <= 0)
					throw new ArgumentOutOfRangeException("BlockSize must be a positive number.");

				if (started)
					throw new InvalidOperationException("Cannot set BlockSize after reading has started.");

				blockSize = value;
			}
		}

		public void Dispose() {
			reader.Dispose();
		}

		public enum CharReaderMode {
			/// <summary>
			/// Sets the reader to read line-by-line.
			/// Will alter newlines and may add or remove a newline at the end of the file - for binary compatibility use Block mode.
			/// </summary>
			Line,
			/// <summary>
			/// Sets the reader to read blocks at a time. Use the BlockSize property to alter block size.
			/// Will preserve binary compatibility but may block (no pun intended) after a newline until enough characters are available, so shouldn't be used for user input.
			/// </summary>
			Block
		}
	}
}
